package lamma.lab.job;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import lamma.helper.ReflectionHelper;
import lamma.lab2.job.AsyncStep;
import lamma.lab2.job.LammaException;
import lamma.lab.job.Result;


/**
 * @author maxcellent
 *
 */
@Deprecated
public class ContextRunner {

	/**
	 * @author maxcellent
	 *
	 */
	private static class ContextField {	
		Class<?> type;
		
		Object value;
		
		boolean ready;
		
		public ContextField(boolean ready, Class<?> type, Object value) {
			super();
			this.ready = ready;
			this.type = type;
			this.value = value;
		}
	}

	private static class Converter {
		Class<?> converterClazz;
		
		String method;
		
		List<ContextField> params;

		ContextField output;
		
		// running status
		boolean isDone;
		
		public Converter(Class<?> converterClazz, String method,
				List<ContextField> params, ContextField output) {
			super();
			this.converterClazz = converterClazz;
			this.method = method;
			this.params = params;
			this.output = output;
			
			this.isDone = false;
		}
		
		public void checkAndConvert() throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
			// check if a job is ready to convert
			for (ContextField param : params) {
				if (!param.ready) {
					return;
				}
			}
			
			// create job instance
			ContextField [] paramsArray = params.toArray(new ContextField[params.size()]);
			Class<?> [] paramTypes = new Class<?> [paramsArray.length];
			Object [] paramVals = new Object[paramsArray.length];
			
			for (int i = 0 ; i < paramsArray.length ; i ++) {
				paramTypes[i] = paramsArray[i].type;
				paramVals[i] = paramsArray[i].value;
			}
			
			Method m = ReflectionHelper.lookupMethod(converterClazz, method, paramTypes);
			
			output.ready = true;
			output.value = m.invoke(null, paramVals);	// invoke static methods
			this.isDone = true;
		}
	}
	
	/**
	 * @author maxcellent
	 *
	 */
	private static class Job {
		
		enum Status {
			INIT, 
			PROCESSING,
			FINISHED
		}
		
		// job identity
		Class<?> jobClazz;
		
		// job identity
		List<JobParam> params;

		// output
		Map<String, ContextField> outputs;	// job results
		
		// running status
		Object jobInstance;
		
		Status status;
		
		Integer currSerial;
		
		SortedMap<Integer, JobStep> stepsMap;
		
		public Job(Class<?> jobClazz, List<JobParam> params) {
			super();
			this.jobClazz = jobClazz;
			this.params = params;
			
			this.outputs = new HashMap<String, ContextField>();
			
			// build running status
			this.status = Status.INIT;
			this.stepsMap = new TreeMap<Integer, JobStep>();
			
			Method [] methods = jobClazz.getDeclaredMethods();
			for (Method method : methods) {
				Step step = method.getAnnotation(Step.class);
				
				if (step != null) {
					this.stepsMap.put(step.serial(), new JobStep(method.getName()));
				}
			}
		}

		public void checkAndRun() throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {		
			switch (this.status) {
			case INIT:
				initJob();
				break;
			case PROCESSING:
				startOrResumeJob();
				break;
			case FINISHED:
				// nothing to do here
				break;
			}
		}

		/**
		 * create job instance
		 * @throws NoSuchMethodException 
		 * @throws SecurityException 
		 * @throws InvocationTargetException 
		 * @throws IllegalAccessException 
		 * @throws InstantiationException 
		 * @throws IllegalArgumentException 
		 * @throws NoSuchFieldException 
		 */
		private void initJob() throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
			// check if a job is ready to run
			for (JobParam param : params) {
				if (!param.field.ready) {
					return;
				}
			}
			
			// create job instance
			JobParam [] paramsArray = params.toArray(new JobParam[params.size()]);
			Class<?> [] paramTypes = new Class<?> [paramsArray.length];
			Object [] paramVals = new Object[paramsArray.length];
			
			for (int i = 0 ; i < paramsArray.length ; i ++) {
				Object val = paramsArray[i].value();
				paramTypes[i] = val.getClass();
				paramVals[i] = val;
			}
			
			Constructor<?> constructor = ReflectionHelper.lookupConstructor(jobClazz, paramTypes);
			
			if (constructor == null) {
				throw new NullPointerException("no applicable constructor found for " + jobClazz + ", please run healthcheck first");
			} 
			
			this.jobInstance = constructor.newInstance(paramVals);
			
			this.startOrResumeJob();
		}

		/**
		 * run the job
		 * @throws NoSuchMethodException 
		 * @throws SecurityException 
		 * @throws InvocationTargetException 
		 * @throws IllegalAccessException 
		 * @throws IllegalArgumentException 
		 * @throws NoSuchFieldException 
		 */
		private void startOrResumeJob() throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
			if (this.currSerial == null) {
				// start job
				this.status = Status.PROCESSING;
				runSteps(stepsMap);
			} else {
				// resume job
				if (stepsMap.get(this.currSerial).isDone()) {
					runSteps(stepsMap.tailMap(this.currSerial + 1));
				}
				// if not done, do nothing here
			}
		}
		
		
		@SuppressWarnings("unchecked")
		private void runSteps(SortedMap<Integer, JobStep> stepsMap) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
			boolean isBlocked = false;
			
			for (Integer serial : stepsMap.keySet()) {
				JobStep currJobStep = stepsMap.get(serial);
				
				Method m = jobClazz.getMethod(currJobStep.methodName);
				Object result = m.invoke(jobInstance);
				
				if (result instanceof AsyncStep) {
					currJobStep.future = (AsyncStep) result;
					isBlocked = true;
					this.currSerial = serial;
					break;	// break the loop once blocked
				} else if (result instanceof Collection) {
					currJobStep.futures = (Collection<AsyncStep>) result;
					isBlocked = true;
					this.currSerial = serial;
					break;	// break the loop once blocked
				}
			}
			
			if (!isBlocked) {	// finishing up the job
				this.status = Status.FINISHED;
				
				// set results to context
				for (String outputFieldName : this.outputs.keySet()) {
					Object val = ReflectionHelper.readField(jobInstance, outputFieldName);

					ContextField contextFiled = this.outputs.get(outputFieldName);
					contextFiled.ready = true;
					contextFiled.value = val;
				}
			}
		}
		
		@Override
		public String toString() {
			return "Job [jobClazz=" + jobClazz + ", params=" + params + "]";
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((jobClazz == null) ? 0 : jobClazz.hashCode());
			result = prime * result + ((params == null) ? 0 : params.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Job other = (Job) obj;
			if (jobClazz == null) {
				if (other.jobClazz != null)
					return false;
			} else if (!jobClazz.equals(other.jobClazz))
				return false;
			if (params == null) {
				if (other.params != null)
					return false;
			} else if (!params.equals(other.params))
				return false;
			return true;
		}
	}
	
	/**
	 * @author maxcellent
	 *
	 *	to be shared by JobRunner as well
	 */
	static class JobStep {
		String methodName;
		
		// runtime status
		AsyncStep future;
		
		Collection<AsyncStep> futures;

		public JobStep(String methodName) {
			super();
			this.methodName = methodName;
		}

		public boolean isDone() {
			if (future == null) {
				for (AsyncStep future : futures) {
					if (!future.isDone()) {	// any task not done, return false
						return false;
					}
				}
				
				return true;
			} else {
				return future.isDone();	
			}
		}
	}
	
	private static class JobParam {
		
		ContextField field;
		
		String [] path;

		public JobParam(ContextField field, String[] path) {
			super();
			this.field = field;
			this.path = path;
		}
		
		/**
		 * TODO : to be replaced by expression language
		 * 
		 * @return
		 * @throws IllegalArgumentException
		 * @throws SecurityException
		 * @throws IllegalAccessException
		 * @throws NoSuchFieldException
		 */
		public Object value() throws IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException {
			Object val = field.value;
			
			for (String pathToken : path) {
				val = ReflectionHelper.readField(val, pathToken);
			}
			
			return val;
		}
	}
	
	/**
	 * 
	 * @param context
	 */
	public Result run(Object context) {
		try {
			this.start(context);
			return new Result(Result.Status.SUCCESS, null);
		} catch (InvocationTargetException e) {
			// exception thrown when the invoked method throws exception 
			return new Result(Result.Status.FAILED, e.getCause());
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
			return new Result(Result.Status.FAILED, 
					new LammaException("Please run health check", e));
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
			return new Result(Result.Status.FAILED, 
					new LammaException("Please run health check", e));
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
			return new Result(Result.Status.FAILED, 
					new LammaException("Please run health check", e));
		} catch (InstantiationException e) {
			e.printStackTrace();
			return new Result(Result.Status.FAILED, 
					new LammaException("Please run health check", e));
		} catch (Throwable e) {
			e.printStackTrace();
			return new Result(Result.Status.FAILED, e);
		}
	}
	
	/**
	 * @param context
	 * @throws IllegalAccessException 
	 * @throws IllegalArgumentException 
	 * @throws InvocationTargetException 
	 * @throws NoSuchMethodException 
	 * @throws SecurityException 
	 * @throws InstantiationException 
	 * @throws NoSuchFieldException 
	 */
	private void start(Object context) throws IllegalArgumentException, IllegalAccessException, SecurityException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
		// ********************* step 1 : build running context *******************
		Map<String, ContextField> fieldsMap = new HashMap<String, ContextField>();
		
		Set<Job> jobs = new HashSet<Job>();	// set of jobs
		Set<Converter> converters = new HashSet<Converter>();	// set of jobs
		
		Class<?> clazz = context.getClass();
		Field [] fields = clazz.getDeclaredFields();
		
		// create all fields
		for (Field field : fields) {
			String fieldName = field.getName();
			Object val = ReflectionHelper.readField(context, field);
			
			FeedBy feedBy = field.getAnnotation(FeedBy.class);
			ConvertBy convertBy = field.getAnnotation(ConvertBy.class);
		
			if (feedBy == null && convertBy == null) {
				// marked all fields not annotated as FeedBy and ConvertBy as READY
				fieldsMap.put(fieldName, new ContextField(true, field.getType(), val));
			} else {
				// all other fields are PEND
				fieldsMap.put(fieldName, new ContextField(false, field.getType(), val));
			}
		}
		
		for (Field field : fields) {
			FeedBy feedBy = field.getAnnotation(FeedBy.class);
			ConvertBy convertBy = field.getAnnotation(ConvertBy.class);
			
			if (feedBy != null) {
				String [] params = feedBy.params();
				
				// use list because input params have to keep their orders
				List<JobParam> paramFields = new ArrayList<JobParam>(params.length);
				for (String param : params) {
					// param may contain a full path, eg, a.b.c 
					// param may contain method, eg, a.b.m1()
					String [] fullPath = FeedBy.Resolver.resolveFullPath(param);
					
					if (fullPath.length > 0) {
						String [] path = new String[fullPath.length - 1];
						System.arraycopy(fullPath, 1, path, 0, path.length);
						
						paramFields.add(new JobParam(fieldsMap.get(fullPath[0]), path));	
					}
				}
				
				// add to jobs' result map
				boolean jobExist = false;
				for (Job job : jobs) {
					if (job.jobClazz.equals(feedBy.job()) && job.params.equals(paramFields)) {
						job.outputs.put(feedBy.result(), fieldsMap.get(field.getName()));
						jobExist = true;
						break;
					}
				}
				
				// create a job if job does not exist
				if (!jobExist) {
					Job job = new Job(feedBy.job(), paramFields);
					job.outputs.put(feedBy.result(), fieldsMap.get(field.getName()));
					jobs.add(job);
				}				
			} else if (convertBy != null) {
				// inject depending params into context fields
				String [] params = convertBy.params();
				
				// use list because input params have to keep their orders
				List<ContextField> paramFields = new ArrayList<ContextField>(params.length);
				for (String param : params) {
					paramFields.add(fieldsMap.get(param));
				}
				
				converters.add(new Converter(convertBy.converter(), convertBy.method(), paramFields, 
						fieldsMap.get(field.getName())));
			}
		}
		
		// ********************* step 2 : kick off all jobs *******************
		while (!jobs.isEmpty() || !converters.isEmpty()) {
			Iterator<Job> i = jobs.iterator();
			while (i.hasNext()) {
				Job job = i.next();
				job.checkAndRun();
				if (job.status == Job.Status.FINISHED) {
					i.remove();
				}
			}
			
			Iterator<Converter> j = converters.iterator();
			while (j.hasNext()) {
				Converter converter = j.next();
				converter.checkAndConvert();
				if (converter.isDone) {
					j.remove();
				}
			}
		}
		
		// ********************* step 3 : set all values back to context *******************
		for (String fieldName : fieldsMap.keySet()) {
			ReflectionHelper.writeField(context, fieldName, fieldsMap.get(fieldName).value);
		}
	}
}